﻿HmJS.$Import('core.core');

/*
---
version: 0.1.2-0
description: A class to represent a range of values, inspired by Ruby's Range class.
license: LGPL
download: http://mootools.net/forge/p/range
source: http://github.com/michaelficarra/MooTools-Range
[Ruby's Range class](http://ruby-doc.org/core/classes/Range.html)

authors:
- Michael Ficarra
requires:
- core/1.2.4:Core
- core/1.2.4:Array
- core/1.2.4:Class
- core/1.2.4:Class.Extras
- range/0.1:Number.Comparable
- range/0.1:String.Comparable
provides: [Range]
...
*/

HmJS.register('util.Range', function ($ns) {

	// #region -- Number.Comparable --

	/*
	---
	description: Provides comparable mixin methods (next,compare) for the Number class.
	license: LGPL
	download: http://mootools.net/forge/p/range
	source: http://github.com/michaelficarra/MooTools-Range

	authors:
	- Michael Ficarra

	requires:
	- core/1.2.4:Core
	provides: [Number.Comparable]
	...
	*/

	Number.implement({

		next: function () {
			return this + 1;
		},

		// UFO operator (<=>)
		compare: function (i) {
			if (i < this) return 1;
			if (i > this) return -1;
			return 0;
		}

	});

	// #endregion

	// #region -- String.Comparable --

	/*
	---
	description: Provides comparable mixin methods (next,compare) for the String class.
	license: LGPL
	download: http://mootools.net/forge/p/range
	source: http://github.com/michaelficarra/MooTools-Range

	authors:
	- Michael Ficarra

	requires:
	- core/1.2.4:Core
	provides: [String.Comparable]
	...
	*/

	String.special_char_order = '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ '.split('');

	String.implement({

		replaceCharAt: function (at, chr) {
			if (at > this.length - 1 || at < this.length * -1) {
				return this;
			}
			if (at >= 0) {
				return this.substr(0, at) + chr + this.substr(at + 1);
			}
			return this.substr(this.length * -1, this.length + at) + chr + (at == -1 ? '' : this.substr(at + 1));
		},

		next: function () {
			if (this == '') { return this; }
			var str = this + '';
			var pos = str.length - 1;
			if (!/[a-z0-9]/i.test(str)) {
				while (pos >= 0) {
					var i = String.special_char_order.indexOf(str[pos]);
					if (i >= 0) {
						str = str.replaceCharAt(pos, String.special_char_order[(i + 1) % String.special_char_order.length]);
						if (i < String.special_char_order.length - 1) {
							break;
						}
					} else {
						str = str.replaceCharAt(pos, String.fromCharCode(str.charCodeAt(pos) + 1));
						break;
					}
					pos--;
				}
				if (pos < 0) {
					str = String.special_char_order[0] + str;
				}
			} else {
				while (pos >= 0) {
					if (!/[a-z0-9]/i.test(str[pos])) {
						pos--;
						continue;
					}
					if (/\d/.test(str[pos])) {
						str = str.replaceCharAt(pos, (str[pos] == '9' ? 0 : parseInt(str[pos], 10) + 1).toString());
						if (str[pos] != '0') { break; }
					} else {
						var c = String.fromCharCode(str.charCodeAt(pos) + 1);
						if (str[pos] == 'z') { c = 'a'; }
						if (str[pos] == 'Z') { c = 'A'; }
						str = str.replaceCharAt(pos, c);
						if (c.toLowerCase() != 'a') { break; }
					}
					pos--;
				}
				if (pos < 0) {
					if (str[0] == '0') {
						str = '1' + str;
					} else if (str[0] == 'a') {
						str = 'a' + str;
					} else if (str[0] == 'A') {
						str = 'A' + str;
					}
				}
			}
			return str;
		},

		// UFO operator (<=>)
		compare: function (s) {
			if (this.length !== s.length) {
				return this.length.compare(s.length);
			}
			if (s < this) { return 1; }
			if (s > this) { return -1; }
			return 0;
		}

	});

	// #endregion

	var $chk = function (obj) {
		return !!(obj || obj === 0);
	};

	return new Class({

		Implements: Options,

		options: {
			inclusive: true,
			nextMethod: null
		},

		initialize: function (begin, end, options) {
			this.begin = begin;
			this.end = $chk(end) ? end : begin;
			this.options.nextMethod = this.begin.next || Function.from($chk(end) ? end : begin);
			this.setOptions(options || {});
			return this;
		},

		first: function () {
			return this.begin;
		},

		last: function () {
			return this.end;
		},

		toArray: function () {
			var currentValue = this.begin;
			this.internalArray = [];
			do {
				this.internalArray.push(currentValue);
				currentValue = this.options.nextMethod.call(currentValue);
			} while (this.end.compare(currentValue) >= (this.options.inclusive ? 0 : 1));
			this.toArray = Function.from(this.internalArray);
			return this.toArray();
		},

		each: function (fn, bind) {
			if ($defined(this.internalArray)) {
				return $each(this.internalArray, fn, bind);
			}
			var index = 0,
					currentValue = this.begin;
			do {
				fn.call(bind, currentValue, index++, this);
				currentValue = this.options.nextMethod.call(currentValue);
			} while (this.end.compare(currentValue) >= (this.options.inclusive ? 0 : 1));
			return this;
		},

		step: function (step, fn, bind) {
			if (step < 1) { return; }
			this.each(function (currentValue, index, range) {
				if ((index % step) == 0) {
					fn.call(bind, currentValue, index / step, range);
				}
			}, bind);
		},

		contains: function (val) {
			return this.begin.compare(val) < 1 && this.end.compare(val) >= (this.options.inclusive ? 0 : 1);
		},

		valueAt: function (index) {
			if ($defined(this.internalArray) || index < 0) {
				var arr = this.toArray();
				if (index.abs() > arr.length) {
					return null;
				}
				return this.toArray().slice(index)[0];
			}
			var currentValue = this.begin;
			for (var i = 0; i < index; i++) {
				currentValue = this.options.nextMethod.call(currentValue);
				if (this.end.compare(currentValue) < (this.options.inclusive ? 0 : 1)) {
					return null;
				}
			};
			return currentValue;
		},

		equals: function (range) {
			return (range.end == this.end
							&& range.begin == this.begin
							&& range.options.inclusive == this.options.inclusive
							&& range.options.nextMethod.toString() == this.options.nextMethod.toString());
		},

		toString: function () { return '(' + this.begin.toString() + (this.options.inclusive ? '..' : '...') + this.end.toString() + ')'; }
	});

});
